local ffi = require "ffi"
local timer = require("timer")
local uuid = require("uuid")

local SESSION_ID_KEY = "LSESSIONID"
local SESSION_SIGNATURE_KEY = "LSESSIONSIGNATURE"

pcall(function ()
    ffi.cdef[[
    void SHA1(unsigned char * t, int len, unsigned char * output_string);
    void EVP_EncodeBlock(unsigned char * t, const unsigned char * f, int len);
    ]]
end)

local ssl
if "Windows" == jit.os then
    ssl = ffi.load("libeay32")
else
    ssl = ffi.load("libssl")
end

local sha1 = function (data)
	local SHA_DIGEST_LENGTH = 20
	local output_string = ffi.new("unsigned char[?]", SHA_DIGEST_LENGTH + 1)
	ssl.SHA1(ffi.cast("unsigned char *", data), string.len(data), output_string)
	local rsl_len = (((string.len(ffi.string(output_string))+2)/3)*4) + 1 --多一个字符
	local result = ffi.new("unsigned char[?]", rsl_len)
	ssl.EVP_EncodeBlock(result, output_string, string.len(ffi.string(output_string)))
	return ffi.string(result)
end

local function genKey()
	uuid.seed()
	return uuid()
end

local TOKEN = string.gsub(genKey(), "-", "")

--对sessionId进行签名
local signSession = function (sessionId, req)
    --把User-Agent请求头的信息用来签名，尽可能的防止伪造请求，冒充sessionId和签名
    local text = sha1(sessionId .. "|" .. req.headers["User-Agent"] .. "|" .. TOKEN)
    return string.gsub(text, "=", "")
end

--设置会话id
local setSessionId = function (req, resp)
    local sessionId = string.gsub(genKey(), "-", "")
    resp:setCookie(SESSION_ID_KEY, sessionId, {
        Path = "/"
    })
    resp:setCookie(SESSION_SIGNATURE_KEY, signSession(sessionId, req), {
        Path = "/"
    })
    req.cookies[SESSION_ID_KEY] = sessionId
    return sessionId
end

--获取请求里面的sessionId
local getSessionId = function (req)
    local sessionId = req.cookies[SESSION_ID_KEY]
    if sessionId then
        local sessionSignature = req.cookies[SESSION_SIGNATURE_KEY]
        if sessionSignature then
            if sessionSignature ~= signSession(sessionId, req) then
                --有sessionId，也有签名，但是签名不正确，sessionId无效，把它清空
                sessionId = nil
            end
        else
            --有sessionId，没有签名，sessionId无效，把它清空
            sessionId = nil
        end
    end
    return sessionId
end

--绑定设置cookie的方法
local bindGetSession = function (req, resp, ctx, opt)
    local ctxData = ctx.data
    if not ctxData then
        ctxData = {}
        ctx.data = ctxData
    end
    local sessionInstMap = ctx.data.sessionInstMap
    if not sessionInstMap then
        sessionInstMap = {}
        ctx.data.sessionInstMap = sessionInstMap
    end
    function req:getSession()
        local sessionId = getSessionId(req)
        if not sessionId then
            --如果没有sessionId，就生成它
            sessionId = setSessionId(req, resp)
        end
        local sessionInst = sessionInstMap[sessionId]
        if not sessionInst then
            sessionInst = {}
            sessionInst.instData = {}
            sessionInst.instMethods = {}
            sessionInst.timer = nil
            sessionInst.instMethods.getSessionId = function ()
                return sessionId
            end
            sessionInst.instMethods.setAttribute = function (key, val)
                sessionInst.instData[key] = val
            end
            sessionInst.instMethods.getAttribute = function (key)
                return sessionInst.instData[key]
            end
            sessionInstMap[sessionId] = sessionInst

            local clearTimer = function ()
                if sessionInst.timer then
                    timer.clearTimeout(sessionInst.timer)
                    sessionInst.timer = nil
                end
            end
            local destroySession = function ()
                local sessionInst = sessionInstMap[sessionId]
                if not sessionInst then
                    --已经销毁了不再销毁
                    return
                end
                clearTimer()
                sessionInst.instMethods.getSessionId = nil
                sessionInst.instMethods.setAttribute = nil
                sessionInst.instMethods.getAttribute = nil
                sessionInst.instMethods = nil
                sessionInst.instData = nil
                sessionInstMap[sessionId] = nil
            end
            sessionInst.instMethods._updateSession = function ()
                if opt and opt.timeout then
                    clearTimer()
                    sessionInst.timer = timer.setTimeout(opt.timeout, destroySession)
                end
            end
        end
        return sessionInst.instMethods
    end
    function ctx:getSessionById(sessionId)
        local sessionInst = sessionInstMap[sessionId]
        if not sessionInst then
            return nil
        else
            return sessionInst.instMethods
        end
    end
end

local M = {}
M.bindGetSession = bindGetSession

local moduleName = ...
_G[moduleName] = M
complex = M
return complex